/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2021-2022. All rights reserved.
 *
 */

import path from 'path';

import { CoreTask, hvigor, HvigorCoreNode, IncrementalExecTask, isLinux, Json5Reader, Module, TaskDetails } from '@ohos/hvigor';
import * as fse from 'fs-extra';

import { ModulePathInfoIml } from '../../common/iml/module-path-info-iml.js';
import { BuildArtifactConst, BuildDirConst } from '../../const/build-directory-const.js';
import { CommonConst, DefaultTargetConst } from '../../const/common-const.js';
import { ApiVersion } from '../../const/sdk-const.js';
import { ModuleType } from '../../enum/module-type-enum.js';
import { CoreModuleModelImpl } from '../../model/module/core-module-model-impl.js';
import { ProjectModel } from '../../model/project/project-model.js';
import { ModuleJson } from '../../options/configure/module-json-options.js';
import { SdkInfo } from '../../sdk/sdk-info.js';
import { FileUtil } from '../../utils/file-util.js';
import { getJson5Obj } from '../../utils/json-util.js';
import { ohPackageLoader } from '../../utils/loader/file/oh-package-loader.js';
import { OhosLogger } from '../../utils/log/ohos-logger.js';
import { pathParamsOpt } from '../../validate/dependency-tag-validate.js';
import { ModuleTargetData } from '../data/hap-task-target-data.js';
import { ModuleTaskService } from '../service/module-task-service.js';
import { TargetTaskService } from '../service/target-task-service.js';
import { OhPackageJsonOpt, PackageJsonOpt } from './ohos-har-task.js';
import { Dependency, DependencyEnum, DependencyType } from '../../project/dependency/core/dependency-interface.js';

/**
 * openHarmony的公共module类型的基础task
 *
 * @since 2022/1/5
 */
export abstract class OhosModuleTask extends IncrementalExecTask {
  public readonly pathInfo: ModulePathInfoIml;
  public moduleModel: CoreModuleModelImpl;
  public targetName: string;
  public targetData: ModuleTargetData;
  protected readonly projectModel: ProjectModel;
  protected module: Module;
  protected moduleName: string;
  protected service: ModuleTaskService;
  public targetService: TargetTaskService;
  protected compileApiVersion: number;
  protected compatibleApiVersion: number;
  protected compatibleSdkVersionStage: string | undefined;
  protected targetSdkVersion: number | undefined;
  protected sdkInfo: SdkInfo;
  protected isFaMode: boolean;
  protected readonly isOhpmProject: boolean;
  protected readonly packageManagerPath: string;
  protected readonly packageJsonObj: PackageJsonOpt | OhPackageJsonOpt;
  protected readonly paramWhiteList: string[] = ['version', 'devDependencies', 'dynamicDependencies', 'dependencies'];
  protected readonly moduleModelNodePaths: Set<string>;
  protected readonly appStartupPath: string | undefined;
  protected readonly appStartupFileName: string | undefined;
  protected readonly intermediateTempStartupFilePath: string;
  private readonly ohosLogger = OhosLogger.getLogger(OhosModuleTask.name);


  /**
   * 当 oh-package.json5 中的依赖是以 @param: 参数化形式填写的本地产物依赖
   * 在打包该 har 模块时需要把本地产物依赖也打进去，所以需要记录依赖的绝对路径，在 AbstractProcessHarArtifacts 中使用
   * 打包非 har 模块或没有使用 @param: 不会有变化
   * @protected
   */
  protected readonly localProductDependenciesPath: string[] = [];

  protected constructor(targetService: TargetTaskService, taskDetails: TaskDetails) {
    super(targetService.getTargetData().getModuleModel().getModule(), targetService.buildTaskName(taskDetails));
    this.module = targetService.getTargetData().getModuleModel().getModule();
    this.moduleName = this.module.getName();
    this.targetService = targetService;
    this.targetData = targetService.getTargetData();
    this.service = targetService.getModuleService();
    this.projectModel = this.service.getProjectModel();
    this.moduleModel = this.service.getModuleModel();
    this.sdkInfo = this.targetService.getSdkInfo();
    this.isFaMode = this.service.isFaMode();
    this.targetName = this.targetService.getTargetData().getTargetName();
    this.pathInfo = targetService.getTargetData().getPathInfo();
    this.compileApiVersion = this.targetData.getCompileApiVersion();
    this.compatibleApiVersion = this.targetData.getCompatibleApiVersion();
    this.targetSdkVersion = this.targetData.getTargetSdkVersion();
    this.compatibleSdkVersionStage = this.targetData.getCompatibleSdkVersionStage();
    this.moduleModelNodePaths = new Set<string>(this.projectModel.getAllModules().map((moduleModel) => moduleModel.getModule().getNodeDir()));
    this.appStartupPath = targetService.getAppStartupPath(this.isFaMode);
    this.appStartupFileName = targetService.getAppStartupFileName(this.isFaMode);
    this.intermediateTempStartupFilePath = this.pathInfo.getIntermediatesStartupPath(this.appStartupFileName);
    this.isOhpmProject = this.projectModel.isOhpmProject();
    this.packageManagerPath = this.isOhpmProject ? this.moduleModel.getOhPackageJson5Path() : this.moduleModel.getPackageJsonPath();

    this.initHarModuleDepends();
    hvigor.nodesEvaluated(() => {
      this.initTaskDependsForOtherModule();
    });
    this.getWorkerPool().warmUp(CommonConst.WARM_UP_SCRIPT);
    if (this.shouldCloseWorkerPoolAfterBuild()) {
      this.getWorkerPool().setResident(false);
    }
    this.packageJsonObj = Json5Reader.getJson5Obj(this.packageManagerPath);
    const parameterFileObj = this.projectModel.getParameterFileObj();
    if (this.isOhpmProject) {
      const pathParamsObj: pathParamsOpt = {
        parameterFilePath: this.projectModel.getParameterFileAbsolutePath(),
        ohPackageFilePath: this.packageManagerPath,
      };

      /**
       * 此处仅替换参数化的版本号，
       * 由于解析参数化的本地依赖需要读取 oh-package.json5，在构造函数中读取 oh-package.json5 可能会出问题
       * 所以此处参数化的本地依赖会保持不变，放到 beforeAction 中进行替换
       */
      FileUtil.resolveJsonObjWithoutParam(this.packageJsonObj, parameterFileObj, pathParamsObj, this.paramWhiteList);
    }
  }

  private shouldCloseWorkerPoolAfterBuild() {
    return isLinux() || this.isFaMode || this.sdkInfo.isOhos || this.sdkInfo.getSdkVersion() < ApiVersion.API_VERSION_11;
  }

  /**
   * 该方法用于添加本模块的任务与其他非library模块之间的任务依赖关系,和处理逻辑
   * 由于需要获取其他模块的信息,因此需要在hvigor的nodeEvaluated方法中先注册,等待所有模块evaluate完后,再执行
   *
   * @protected
   */
  protected initTaskDependsForOtherModule() {
    // default no depends
  }

  /**
   * 该方法只能添加本模块自身的task之间的任务依赖关系,其中的处理逻辑不需要依赖其他模块的任务数据
   *
   * @protected
   */
  public abstract initTaskDepends(): void;

  /**
   * 该方法用于添加本模块与本地library模块之间的任务依赖关系
   *
   * @protected
   */
  protected initHarModuleDepends() {
    // default no depends
  }

  /**
   * 为每个target执行task的具体逻辑
   */
  protected abstract doTaskAction(): void;

  /**
   * 在task执行之前做hook, 通常用于clean上次构建的输出
   */
  protected beforeTask(): void {
    if (this.isOhpmProject) {
      const pathParamsObj: pathParamsOpt = {
        parameterFilePath: this.projectModel.getParameterFileAbsolutePath(),
        ohPackageFilePath: this.packageManagerPath,
      };

      /**
       * 解析参数化的本地依赖需要读取 oh-package.json5，因此不能放在构造函数中
       */
      const localDepArr = FileUtil.resolveJsonObjWithoutParam(
        this.packageJsonObj,
        this.projectModel.getParameterFileObj(),
        pathParamsObj,
        this.paramWhiteList,
        (ohPackageJsonAbsolutePath: string) => ohPackageLoader.getOhPackageJsonObj(ohPackageJsonAbsolutePath) as any,
      );
      this.localProductDependenciesPath.push(...localDepArr);
    }
  }

  /**
   * 根据targetData状态判断是否跳过该task
   */
  taskShouldDo(): boolean {
    return true;
  }

  registryAction = (): Function => {
    return async () => {
      await this.beforeTask();
      if (!this.taskShouldDo()) {
        return;
      }
      await this.doTaskAction();
    };
  };

  /**
   * 获取每个target的临时目录(缓存目录)
   * 调该方法时如果该目录不存在，则创建新目录
   *
   * @param {string} targetData targets数据对象
   * @param {string} tempDirName 拼接目录名称
   * @param {boolean} isCreateDir 若目录不存在，是否创建新目录
   * @protected
   */
  protected getTaskTempDir(targetData: ModuleTargetData, tempDirName?: string, isCreateDir = true): string {
    const tempDir: string = path.resolve(targetData.getPathInfo().getModuleBuildCachePath(), tempDirName ? tempDirName : this.name);
    if (isCreateDir && !fse.existsSync(tempDir)) {
      fse.mkdirsSync(tempDir);
    }
    return tempDir;
  }

  protected getImporteesCachePath(cacheFileName: string, arkCompileName: string): string {
    const targetData = this.targetService.getTargetData();
    const compileMode = this.moduleModel.getCompileMode(targetData.getProduct());
    return path.resolve(
      this.getTaskTempDir(targetData, `${this.targetName}@${arkCompileName}`, false),
      compileMode,
      this.targetService.getBuildMode().toLowerCase(),
      cacheFileName,
    );
  }

  /**
   * 根据编译期间收集到的importee，找到对应的依赖，并由回调进行处理
   * @param filePath
   * @param dependencies
   * @param callback
   * @protected
   */
  protected processImportees(filePath: string, dependencies: Dependency[], callback: Function) {
    if (!fse.existsSync(filePath)) {
      return;
    }
    const ary = getJson5Obj(filePath);
    if (!Array.isArray(ary)) {
      return;
    }
    ary.forEach((importee) => {
      const dependency = dependencies.find((dep) => {
        return importee === dep.getDependencyName() || importee.startsWith(`${dep.getDependencyName()}/`);
      });
      if (!dependency) {
        return;
      }
      callback(importee, dependency.getDependencyType(), dependency);
    });
  }

  /**
   * 根据优先级合并后，获取全局依赖的对应的依赖别名
   * @protected
   */
  protected getGlobalDependencyKeys() {
    const root = this.moduleModel.getModule().getNodeDir();
    return this.service.getAllDirectPkgNodes()
      .filter(node => !!node.parent && node.parent.npm.getDependencyRootPath() !== root)
      .map(node => node.npm.getDependencyName());
  }

  /**
   * 收集参与编译的一些依赖信息
   * @protected
   * @return
   *   compileDependencyKeys: 参与编译的依赖别名（自身+工程级）
   *   globalCompileDependencyKeys：参与编译的依赖别名（工程级）
   */
  protected collectDirectAffectsCompilationDependencyKeys(importeesPath: string) {
    const globalDependencyKeys = this.getGlobalDependencyKeys();
    const compileDependencyKeySet = new Set<string>(this.targetService.getBuildOption()?.arkOptions?.runtimeOnly?.packages);
    this.processImportees(importeesPath, this.service.getAllDirectDependencies(), (importee: string, dependencyType: DependencyType, dependency: Dependency) => {
      compileDependencyKeySet.add(dependency.getDependencyName());
    });
    const compileDependencyKeys = Array.from(compileDependencyKeySet);
    const globalCompileDependencyKeys = compileDependencyKeys.filter(v => globalDependencyKeys.includes(v));
    return { compileDependencyKeys, globalCompileDependencyKeys };
  }

  /**
   * 针对以下情况，需要做warning提示：
   * 1.编译HSP/bundled的字节码HAR，若使用到工程级本地HAR依赖，需要给出warning
   * @param importeesPath
   * @protected
   */
  protected doGlobalLocalDependencyCheck(importeesPath: string) {
    if (this.targetName === DefaultTargetConst.OHOS_TEST_TARGET) {
      return;
    }
    const { compileDependencyKeys, globalCompileDependencyKeys} = this.collectDirectAffectsCompilationDependencyKeys(importeesPath);
    const globalLocalDependencies = new Set<string>();
    this.service.getAllDirectDependencies().forEach(dependency => {
      const dependencyKey = dependency.getDependencyName();
      if (!compileDependencyKeys.includes(dependencyKey)) {
        return;
      }
      if (globalCompileDependencyKeys.includes(dependencyKey) && dependency.isLocal()) {
        globalLocalDependencies.add(dependencyKey);
      }
    });
    if (globalLocalDependencies.size) {
      this.ohosLogger.warn(
        `You have used local dependencies [${[...globalLocalDependencies].join(',')}]. These dependencies are not configured in the oh-package.json5 file of the current module, therefore, the build system does not collect their libs, resources, and related configurations (such as dynamicImport, routerMap, and abilities) during the build process. As a result, a runtime exception may occur during subsequent integration. You are advised to configure these dependencies in the oh-package.json5 file of the current module.`
      );
    }
  }

  /**
   * 针对以下情况，需要做warning提示：
   *  1.编译HSP/HSP/bundled的字节码HAR，若使用到devDependencies中的HAR依赖，需要给出warning
   * @param importeesPath
   * @protected
   */
  protected doDevDependencyCheck( importeesPath: string) {
    if (this.targetName === DefaultTargetConst.OHOS_TEST_TARGET) {
      return;
    }
    const { compileDependencyKeys} = this.collectDirectAffectsCompilationDependencyKeys(importeesPath);
    const devDependencies = new Set<string>();
    this.service.getAllDirectDependencies().forEach(dependency => {
      const dependencyKey = dependency.getDependencyName();
      if (!compileDependencyKeys.includes(dependencyKey)) {
        return;
      }
      if (dependency.getDependencyEnum() === DependencyEnum.DEV_DEPENDENCIES) {
        devDependencies.add(dependencyKey);
      }
    });
    if (devDependencies.size) {
      this.ohosLogger.warn(`The dependencies [${[...devDependencies].join(',')}] you are using are listed under devDependencies. As such, the build system won't collect their libs, resources, and related configurations (for dynamicImport, routerMap, abilities, and more) during the build process. This could cause runtime issues later. To avoid these issues, move these dependencies to 'dependencies'.`)
    }
  }

  /**
   * 替换.preview产物中的pages文件的src
   * 保证路由/非路由页面预览pages.json中配置一致
   *
   * @param {OhosLogger} log
   * @param {string[]} pages 预览传入当前实际预览页面src
   * @protected
   */
  protected replacePagesInPreview(log: OhosLogger, pages: string[]): void {
    const moduleJsonPath: string = path.resolve(this.pathInfo.getIntermediatesRes(), CommonConst.MODULE_JSON);
    const moduleJsonObj: ModuleJson.ModuleOptObj = getJson5Obj(moduleJsonPath);
    const pagesJsonPath = path.resolve(
      this.projectModel.getProjectDir(),
      this.moduleModel.getName(),
      `src/main/${BuildDirConst.RESOURCES_BASE_PROFILE_DIR}/${BuildArtifactConst.MAIN_PAGES_JSON}`,
    );
    if (this.moduleModel.getModuleType() === ModuleType.Shared && !fse.existsSync(pagesJsonPath)) {
      // hsp不存在main_pages.json文件时,需在中间产物资源目录下生成并将引用写入module.json
      const intermediatesMainPageJsonPath = this.pathInfo.getPreviewIntermediatesMainPagesJsonPath();
      fse.writeJSONSync(intermediatesMainPageJsonPath, {
        src: ['pages/Index'],
      });
    }
    const pagesStr: string | undefined = moduleJsonObj.module.pages;
    const pageJsonFileName = `${pagesStr.replace(/\$profile:/, '')}.json`;
    const pageJsonFilePath: string = path.resolve(this.pathInfo.getIntermediatesResProfilePath(), pageJsonFileName);
    const mainPagesJson = fse.readJSONSync(pageJsonFilePath);

    // 当前实际预览页面src替换main_pages.json文件中src配置
    mainPagesJson.src = pages;
    log.debug(`Resolved file ${pageJsonFilePath}.`);
    log.debug(`Replace page to ${pages}`);
    fse.outputJSONSync(pageJsonFilePath, mainPagesJson);
  }

  /**
   * 替换module.json5中配置的pages信息
   * 该方法需要在stage模型的资源编译后才可调用
   *
   * @param {OhosLogger} log
   * @param {string[]} pages
   * @protected
   */
  protected replacePages(log: OhosLogger, pages: string[]): void {
    const pageModel: object = { src: pages };
    const pageJsonFileName = this.getPageJsonFileName(log);
    const pageJsonFilePath: string = path.resolve(this.pathInfo.getIntermediatesResProfilePath(), pageJsonFileName);
    log.debug(`Resolved file ${pageJsonFilePath}.`);
    log.debug(`Replace page to ${pages}`);
    fse.outputJSONSync(pageJsonFilePath, pageModel);
  }

  /**
   * 获取profile pages路由配置文件名
   *
   * @param {OhosLogger} log
   * @return {string} profile路由配置文件名称
   * @protected
   */
  protected getPageJsonFileName(log: OhosLogger): string {
    const moduleJsonPath: string = path.resolve(this.pathInfo.getIntermediatesRes(), CommonConst.MODULE_JSON);
    const moduleJsonObj: ModuleJson.ModuleOptObj = getJson5Obj(moduleJsonPath);

    // $profile:xxxxxx
    const pagesStr: string | undefined = moduleJsonObj.module.pages;

    // schema已经进行了校验,这里防止undefined 兜底校验
    if (!pagesStr) {
      log.printErrorExit('UNABLE_TO_REPLACE_PAGES_CANNOT_FIND_PAGES', [this.moduleModel.getName()]);
    }
    return `${pagesStr.replace(/\$profile:/, '')}.json`;
  }

  /**
   * 解析模块级/工程级的oh-package.json5/package.json获取其dependencies或devDependencies
   * @param model CoreModuleModelImpl | ProjectModel
   * @param needDev
   * @private
   */
  protected getDependencies(model: CoreModuleModelImpl | ProjectModel, needDev: boolean): Record<string, string> {
    const pkgJsonPath = this.projectModel.isOhpmProject() ? model.getOhPackageJson5Path() : model.getPackageJsonPath();
    let dependencies: Record<string, string> = {};
    if (fse.existsSync(pkgJsonPath)) {
      const pkgJsonObj = Json5Reader.getJson5Obj(pkgJsonPath);
      dependencies = {
        dependency: dependencies,
        ...pkgJsonObj.dependencies,
      };
      if (needDev) {
        Object.assign(dependencies, pkgJsonObj.devDependencies);
      }
    }

    const ordered: Record<string, string> = {};
    Object.keys(dependencies)
      .sort()
      .forEach((key) => (ordered[key] = dependencies[key]));
    return ordered;
  }

  protected getDynamicDependencies(model: CoreModuleModelImpl | ProjectModel): Record<string, string> {
    const pkgJsonPath = this.projectModel.isOhpmProject() ? model.getOhPackageJson5Path() : model.getPackageJsonPath();
    let dependencies: Record<string, string> = {};
    if (fse.existsSync(pkgJsonPath)) {
      try {
        const pkgJsonObj = Json5Reader.getJson5Obj(pkgJsonPath);
        dependencies = {
          dependency: dependencies,
          ...pkgJsonObj.dynamicDependencies,
        };
      } catch (e: any) {
        this.ohosLogger.warn('Failed to read json5:', e.message);
      }
    }

    const ordered: Record<string, string> = {};
    Object.keys(dependencies)
      .sort()
      .forEach((key) => (ordered[key] = dependencies[key]));
    return ordered;
  }

  declareDepends(task: string | CoreTask, node?: string | HvigorCoreNode): CoreTask {
    let taskName: string;
    if (task instanceof CoreTask) {
      taskName = task.getName();
    } else {
      taskName = task;
    }
    if (taskName.split('@').length > 1) {
      return super.dependsOn(taskName, node);
    }
    taskName = `${this.targetName}@${taskName}`;
    return super.dependsOn(taskName, node);
  }

  declareDependsList(...tasks: (string | CoreTask)[]) {
    tasks.forEach((task) => {
      this.declareDepends(task);
    });
  }

  dependsOnHook(task: string | CoreTask, node?: string | HvigorCoreNode) {
    let taskName: string;
    if (task instanceof CoreTask) {
      taskName = task.getName();
    } else {
      taskName = task;
    }
    return super.dependsOn(taskName, node);
  }

  // 判断stage闭源包里 是否有UseTsHar 为 true 的配置，如果有 index 后缀改为.ts
  protected hasUseTsHarField(): boolean {
    // 字节码har默认为UseTsHar
    if (this.targetService.isByteCodeHar()) {
      return true;
    }
    if (!this.isFaMode) {
      const jsonProfileOpt = this.moduleModel.getJsonProfileOptByTargetName(this.targetName, false) as ModuleJson.ModuleOptObj;

      const useTsHar = jsonProfileOpt.module.metadata?.find((item) => item.name === CommonConst.USE_TS_HAR && item.value === 'true');
      return useTsHar !== undefined;
    }
    return false;
  }
}
